// MIT License

// Copyright (c) 2019 Erin Catto

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#define _CRT_SECURE_NO_WARNINGS
#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS 1

#include "imgui/imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "draw.h"
#include "settings.h"
#include "test.h"

#include <algorithm>
#include <stdio.h>
#include <thread>
#include <chrono>

#if defined(_WIN32)
#include <crtdbg.h>
#endif

GLFWwindow* g_mainWindow = nullptr;
static int32 s_testSelection = 0;
static Test* s_test = nullptr;
static Settings s_settings;
static bool s_rightMouseDown = false;
static b2Vec2 s_clickPointWS = b2Vec2_zero;

void glfwErrorCallback(int error, const char* description)
{
  fprintf(stderr, "GLFW error occured. Code: %d. Description: %s\n", error, description);
}

static inline bool CompareTests(const TestEntry& a, const TestEntry& b)
{
  int result = strcmp(a.category, b.category);
  if (result == 0)
  {
    result = strcmp(a.name, b.name);
  }

  return result < 0;
}

static void SortTests()
{
  std::sort(g_testEntries, g_testEntries + g_testCount, CompareTests);
}

static void CreateUI(GLFWwindow* window, const char* glslVersion = NULL)
{
  IMGUI_CHECKVERSION();
  ImGui::CreateContext();

  bool success;
  success = ImGui_ImplGlfw_InitForOpenGL(window, false);
  if (success == false)
  {
    printf("ImGui_ImplGlfw_InitForOpenGL failed\n");
    assert(false);
  }

  success = ImGui_ImplOpenGL3_Init(glslVersion);
  if (success == false)
  {
    printf("ImGui_ImplOpenGL3_Init failed\n");
    assert(false);
  }

  // Search for font file
  const char* fontPath1 = "data/droid_sans.ttf";
  const char* fontPath2 = "../data/droid_sans.ttf";
  const char* fontPath = nullptr;
  FILE* file1 = fopen(fontPath1, "rb");
  FILE* file2 = fopen(fontPath2, "rb");
  if (file1)
  {
    fontPath = fontPath1;
    fclose(file1);
  }
  
  if (file2)
  {
    fontPath = fontPath2;
    fclose(file2);
  }

  if (fontPath)
  {
    ImGui::GetIO().Fonts->AddFontFromFileTTF(fontPath, 13.0f);
  }
}

static void ResizeWindowCallback(GLFWwindow*, int width, int height)
{
  g_camera.m_width = width;
  g_camera.m_height = height;
  s_settings.m_windowWidth = width;
  s_settings.m_windowHeight = height;
}

static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
  ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
  if (ImGui::GetIO().WantCaptureKeyboard)
  {
    return;
  }

  if (action == GLFW_PRESS)
  {
    switch (key)
    {
    case GLFW_KEY_ESCAPE:
      // Quit
      glfwSetWindowShouldClose(g_mainWindow, GL_TRUE);
      break;

    case GLFW_KEY_LEFT:
      // Pan left
      if (mods == GLFW_MOD_CONTROL)
      {
        b2Vec2 newOrigin(2.0f, 0.0f);
        s_test->ShiftOrigin(newOrigin);
      }
      else
      {
        g_camera.m_center.x -= 0.5f;
      }
      break;

    case GLFW_KEY_RIGHT:
      // Pan right
      if (mods == GLFW_MOD_CONTROL)
      {
        b2Vec2 newOrigin(-2.0f, 0.0f);
        s_test->ShiftOrigin(newOrigin);
      }
      else
      {
        g_camera.m_center.x += 0.5f;
      }
      break;

    case GLFW_KEY_DOWN:
      // Pan down
      if (mods == GLFW_MOD_CONTROL)
      {
        b2Vec2 newOrigin(0.0f, 2.0f);
        s_test->ShiftOrigin(newOrigin);
      }
      else
      {
        g_camera.m_center.y -= 0.5f;
      }
      break;

    case GLFW_KEY_UP:
      // Pan up
      if (mods == GLFW_MOD_CONTROL)
      {
        b2Vec2 newOrigin(0.0f, -2.0f);
        s_test->ShiftOrigin(newOrigin);
      }
      else
      {
        g_camera.m_center.y += 0.5f;
      }
      break;

    case GLFW_KEY_HOME:
      // Reset view
      g_camera.m_zoom = 1.0f;
      g_camera.m_center.Set(0.0f, 20.0f);
      break;

    case GLFW_KEY_Z:
      // Zoom out
      g_camera.m_zoom = b2Min(1.1f * g_camera.m_zoom, 20.0f);
      break;

    case GLFW_KEY_X:
      // Zoom in
      g_camera.m_zoom = b2Max(0.9f * g_camera.m_zoom, 0.02f);
      break;

    case GLFW_KEY_R:
      // Reset test
      delete s_test;
      s_test = g_testEntries[s_settings.m_testIndex].createFcn();
      break;

    case GLFW_KEY_SPACE:
      // Launch a bomb.
      if (s_test)
      {
        s_test->LaunchBomb();
      }
      break;

    case GLFW_KEY_O:
      s_settings.m_singleStep = true;
      break;

    case GLFW_KEY_P:
      s_settings.m_pause = !s_settings.m_pause;
      break;

    case GLFW_KEY_LEFT_BRACKET:
      // Switch to previous test
      --s_testSelection;
      if (s_testSelection < 0)
      {
        s_testSelection = g_testCount - 1;
      }
      break;

    case GLFW_KEY_RIGHT_BRACKET:
      // Switch to next test
      ++s_testSelection;
      if (s_testSelection == g_testCount)
      {
        s_testSelection = 0;
      }
      break;

    case GLFW_KEY_TAB:
      g_debugDraw.m_showUI = !g_debugDraw.m_showUI;

    default:
      if (s_test)
      {
        s_test->Keyboard(key);
      }
    }
  }
  else if (action == GLFW_RELEASE)
  {
    s_test->KeyboardUp(key);
  }
  // else GLFW_REPEAT
}

static void CharCallback(GLFWwindow* window, unsigned int c)
{
  ImGui_ImplGlfw_CharCallback(window, c);
}

static void MouseButtonCallback(GLFWwindow* window, int32 button, int32 action, int32 mods)
{
  ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods);

  double xd, yd;
  glfwGetCursorPos(g_mainWindow, &xd, &yd);
  b2Vec2 ps((float)xd, (float)yd);

  // Use the mouse to move things around.
  if (button == GLFW_MOUSE_BUTTON_1)
  {
        //<##>
        //ps.Set(0, 0);
    b2Vec2 pw = g_camera.ConvertScreenToWorld(ps);
    if (action == GLFW_PRESS)
    {
      if (mods == GLFW_MOD_SHIFT)
      {
        s_test->ShiftMouseDown(pw);
      }
      else
      {
        s_test->MouseDown(pw);
      }
    }
    
    if (action == GLFW_RELEASE)
    {
      s_test->MouseUp(pw);
    }
  }
  else if (button == GLFW_MOUSE_BUTTON_2)
  {
    if (action == GLFW_PRESS)
    {	
      s_clickPointWS = g_camera.ConvertScreenToWorld(ps);
      s_rightMouseDown = true;
    }

    if (action == GLFW_RELEASE)
    {
      s_rightMouseDown = false;
    }
  }
}

static void MouseMotionCallback(GLFWwindow*, double xd, double yd)
{
  b2Vec2 ps((float)xd, (float)yd);

  b2Vec2 pw = g_camera.ConvertScreenToWorld(ps);
  s_test->MouseMove(pw);
  
  if (s_rightMouseDown)
  {
    b2Vec2 diff = pw - s_clickPointWS;
    g_camera.m_center.x -= diff.x;
    g_camera.m_center.y -= diff.y;
    s_clickPointWS = g_camera.ConvertScreenToWorld(ps);
  }
}

static void ScrollCallback(GLFWwindow* window, double dx, double dy)
{
  ImGui_ImplGlfw_ScrollCallback(window, dx, dy);
  if (ImGui::GetIO().WantCaptureMouse)
  {
    return;
  }

  if (dy > 0)
  {
    g_camera.m_zoom /= 1.1f;
  }
  else
  {
    g_camera.m_zoom *= 1.1f;
  }
}

static void RestartTest()
{
  delete s_test;
  s_test = g_testEntries[s_settings.m_testIndex].createFcn();
}

static void UpdateUI()
{
  int menuWidth = 180;
  if (g_debugDraw.m_showUI)
  {
    ImGui::SetNextWindowPos(ImVec2((float)g_camera.m_width - menuWidth - 10, 10));
    ImGui::SetNextWindowSize(ImVec2((float)menuWidth, (float)g_camera.m_height - 20));

    ImGui::Begin("Tools", &g_debugDraw.m_showUI, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);

    if (ImGui::BeginTabBar("ControlTabs", ImGuiTabBarFlags_None))
    {
      if (ImGui::BeginTabItem("Controls"))
      {
        ImGui::SliderInt("Vel Iters", &s_settings.m_velocityIterations, 0, 50);
        ImGui::SliderInt("Pos Iters", &s_settings.m_positionIterations, 0, 50);
        ImGui::SliderFloat("Hertz", &s_settings.m_hertz, 5.0f, 120.0f, "%.0f hz");
        
        ImGui::Separator();

        ImGui::Checkbox("Sleep", &s_settings.m_enableSleep);
        ImGui::Checkbox("Warm Starting", &s_settings.m_enableWarmStarting);
        ImGui::Checkbox("Time of Impact", &s_settings.m_enableContinuous);
        ImGui::Checkbox("Sub-Stepping", &s_settings.m_enableSubStepping);

        ImGui::Separator();

        ImGui::Checkbox("Shapes", &s_settings.m_drawShapes);
        ImGui::Checkbox("Joints", &s_settings.m_drawJoints);
        ImGui::Checkbox("Particles", &s_settings.m_drawParticles);
        ImGui::Checkbox("AABBs", &s_settings.m_drawAABBs);
        ImGui::Checkbox("Contact Points", &s_settings.m_drawContactPoints);
        ImGui::Checkbox("Contact Normals", &s_settings.m_drawContactNormals);
        ImGui::Checkbox("Contact Impulses", &s_settings.m_drawContactImpulse);
        ImGui::Checkbox("Friction Impulses", &s_settings.m_drawFrictionImpulse);
        ImGui::Checkbox("Center of Masses", &s_settings.m_drawCOMs);
        ImGui::Checkbox("Statistics", &s_settings.m_drawStats);
        ImGui::Checkbox("Profile", &s_settings.m_drawProfile);

        ImVec2 button_sz = ImVec2(-1, 0);
        if (ImGui::Button("Pause (P)", button_sz))
        {
          s_settings.m_pause = !s_settings.m_pause;
        }

        if (ImGui::Button("Single Step (O)", button_sz))
        {
          s_settings.m_singleStep = !s_settings.m_singleStep;
        }

        if (ImGui::Button("Restart (R)", button_sz))
        {
          RestartTest();
        }

        if (ImGui::Button("Quit", button_sz))
        {
          glfwSetWindowShouldClose(g_mainWindow, GL_TRUE);
        }

        ImGui::EndTabItem();
      }

      ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
      leafNodeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;

      ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;

      if (ImGui::BeginTabItem("Tests"))
      {
        int categoryIndex = 0;
        const char* category = g_testEntries[categoryIndex].category;
        int i = 0;
        while (i < g_testCount)
        {
          bool categorySelected = strcmp(category, g_testEntries[s_settings.m_testIndex].category) == 0;
          ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags_Selected : 0;
          bool nodeOpen = ImGui::TreeNodeEx(category, nodeFlags | nodeSelectionFlags);

          if (nodeOpen)
          {
            while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0)
            {
              ImGuiTreeNodeFlags selectionFlags = 0;
              if (s_settings.m_testIndex == i)
              {
                selectionFlags = ImGuiTreeNodeFlags_Selected;
              }
              ImGui::TreeNodeEx((void*)(intptr_t)i, leafNodeFlags | selectionFlags, "%s", g_testEntries[i].name);
              if (ImGui::IsItemClicked())
              {
                delete s_test;
                s_settings.m_testIndex = i;
                s_test = g_testEntries[i].createFcn();
                s_testSelection = i;
              }
              ++i;
            }
            ImGui::TreePop();
          }
          else
          {
            while (i < g_testCount && strcmp(category, g_testEntries[i].category) == 0)
            {
              ++i;
            }
          }

          if (i < g_testCount)
          {
            category = g_testEntries[i].category;
            categoryIndex = i;
          }
        }
        ImGui::EndTabItem();
      }
      ImGui::EndTabBar();
    }

    ImGui::End();

    s_test->UpdateUI();
  }
}

//
int main(int, char**)
{
#if defined(_WIN32)
  // Enable memory-leak reports
  _CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG));
#endif

  char buffer[128];

  s_settings.Load();
  SortTests();

  glfwSetErrorCallback(glfwErrorCallback);

  g_camera.m_width = s_settings.m_windowWidth;
  g_camera.m_height = s_settings.m_windowHeight;

  if (glfwInit() == 0)
  {
    fprintf(stderr, "Failed to initialize GLFW\n");
    return -1;
  }

#if __APPLE__
    const char* glslVersion = "#version 150";
#else
    const char* glslVersion = NULL;
#endif

  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  sprintf(buffer, "Box2D Testbed Version %d.%d.%d", b2_version.major, b2_version.minor, b2_version.revision);

  bool fullscreen = false;
  if (fullscreen)
  {
    g_mainWindow = glfwCreateWindow(1920, 1080, buffer, glfwGetPrimaryMonitor(), NULL);
  }
  else
  {
    g_mainWindow = glfwCreateWindow(g_camera.m_width, g_camera.m_height, buffer, NULL, NULL);
  }

  if (g_mainWindow == NULL)
  {
    fprintf(stderr, "Failed to open GLFW g_mainWindow.\n");
    glfwTerminate();
    return -1;
  }

  glfwMakeContextCurrent(g_mainWindow);

  // Load OpenGL functions using glad
  int version = gladLoadGL(glfwGetProcAddress);
  printf("GL %d.%d\n", GLAD_VERSION_MAJOR(version), GLAD_VERSION_MINOR(version));
  printf("OpenGL %s, GLSL %s\n", glGetString(GL_VERSION), glGetString(GL_SHADING_LANGUAGE_VERSION));

  glfwSetScrollCallback(g_mainWindow, ScrollCallback);
  glfwSetWindowSizeCallback(g_mainWindow, ResizeWindowCallback);
  glfwSetKeyCallback(g_mainWindow, KeyCallback);
  glfwSetCharCallback(g_mainWindow, CharCallback);
  glfwSetMouseButtonCallback(g_mainWindow, MouseButtonCallback);
  glfwSetCursorPosCallback(g_mainWindow, MouseMotionCallback);
  glfwSetScrollCallback(g_mainWindow, ScrollCallback);

  g_debugDraw.Create();

  CreateUI(g_mainWindow, glslVersion);

  s_settings.m_testIndex = b2Clamp(s_settings.m_testIndex, 0, g_testCount - 1);
  s_testSelection = s_settings.m_testIndex;
  s_test = g_testEntries[s_settings.m_testIndex].createFcn();

  // Control the frame rate. One draw per monitor refresh.
  //glfwSwapInterval(1);

  glClearColor(0.2f, 0.2f, 0.2f, 1.0f);

  std::chrono::duration<double> frameTime(0.0);
  std::chrono::duration<double> sleepAdjust(0.0);

  while (!glfwWindowShouldClose(g_mainWindow))
  {
    std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();

    glfwGetWindowSize(g_mainWindow, &g_camera.m_width, &g_camera.m_height);
        
        int bufferWidth, bufferHeight;
        glfwGetFramebufferSize(g_mainWindow, &bufferWidth, &bufferHeight);
        glViewport(0, 0, bufferWidth, bufferHeight);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();

    ImGui::NewFrame();

    if (g_debugDraw.m_showUI)
    {
      ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
      ImGui::SetNextWindowSize(ImVec2(float(g_camera.m_width), float(g_camera.m_height)));
      ImGui::SetNextWindowBgAlpha(0.0f);
      ImGui::Begin("Overlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar);
      ImGui::End();

      const TestEntry& entry = g_testEntries[s_settings.m_testIndex];
      sprintf(buffer, "%s : %s", entry.category, entry.name);
      s_test->DrawTitle(buffer);
    }

    s_test->Step(s_settings);

    UpdateUI();

    // ImGui::ShowDemoWindow();
      
    if (g_debugDraw.m_showUI)
    {
      sprintf(buffer, "%.1f ms", 1000.0 * frameTime.count());
      g_debugDraw.DrawString(5, g_camera.m_height - 20, buffer);
    }

    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    glfwSwapBuffers(g_mainWindow);

    if (s_testSelection != s_settings.m_testIndex)
    {
      s_settings.m_testIndex = s_testSelection;
      delete s_test;
      s_test = g_testEntries[s_settings.m_testIndex].createFcn();
      g_camera.m_zoom = 1.0f;
      g_camera.m_center.Set(0.0f, 20.0f);
    }

    glfwPollEvents();

    // Throttle to cap at 60Hz. This adaptive using a sleep adjustment. This could be improved by
    // using mm_pause or equivalent for the last millisecond.
    std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
    std::chrono::duration<double> target(1.0 / 60.0);
    std::chrono::duration<double> timeUsed = t2 - t1;
    std::chrono::duration<double> sleepTime = target - timeUsed + sleepAdjust;
    if (sleepTime > std::chrono::duration<double>(0))
    {
      std::this_thread::sleep_for(sleepTime);
    }

    std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now();
    frameTime = t3 - t1;

    // Compute the sleep adjustment using a low pass filter
    sleepAdjust = 0.9 * sleepAdjust + 0.1 * (target - frameTime);
  }

  delete s_test;
  s_test = nullptr;

  g_debugDraw.Destroy();
  ImGui_ImplOpenGL3_Shutdown();
  ImGui_ImplGlfw_Shutdown();
  glfwTerminate();

  s_settings.Save();

  return 0;
}
